承接上一篇在 HTML 宣告 <link>,這篇要來介紹一個更快的做法:在 HTTP response header 宣告 Link
翻開 MDN 文件 的話,會發現其實各瀏覽器大約都在 2022 ~ 2023 年才開始支援在 HTTP response header 設定 <Link>,其實算是蠻新的功能。我自己也只有在 hackerone 看到有設定

由於 iThome 使用 cloudflare,發文若有一些 XSS 的關鍵字會被擋下,所以本文若有用到 script 標籤,都會改成 <xcript>
以上圖 hackerone 的網站為例
</assets/static/main_css-ClkKLtaZ.css>; rel=preload; as=style; nopush
前面三個區塊,我想應該沒什麼問題,等同於
<link href="/assets/static/main_css-ClkKLtaZ.css" rel="preload" as="style" />
至於 nopush,這是 HTTP/2 的語法,本篇暫時不討論
<link>?簡單講,就是因為 HTTP Link 可以比較快載入。假設某網站的首頁回傳的 response headers 為
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Link: </assets/static/main_css-ClkKLtaZ.css>; rel=preload; as=style
瀏覽器收到 response headers 的當下,就可以開始 preload 對應的資源,不用等 HTML 回傳!
上一篇文章,談了很多 <link rel> 的用法,但根據 MDN 文件,能用在 HTTP Link 的只有
簡單理解的話,因為 HTTP Link(response header)是在 HTML(response body)之前抵達
所以那些設定在 HTML 身上,例如 <link rel="icon" href="favicon.ico">,主流瀏覽器不一定有支援
Link: <https://example.com>; rel="preconnect"
Link: <https://example.com/%E5%B8%A5%E5%93%A5>; rel="preconnect" // 帥哥
Link: <https://one.example.com>; rel="preconnect", <https://two.example.com>; rel="preconnect"
Link: </style.css>; rel=preload; as=style; fetchpriority="high"
使用 Node.js http 模組,測試是否真的有建立 TCP 連線
import http from "http";
const httpServer5000 = http.createServer((req, res) => {
const url = new URL(req.url || "", "http://localhost:5000");
if (url.pathname === "/preconnect") {
res.setHeader("link", `<http://localhost:5001>; rel="preconnect"`);
res.end();
return;
}
});
httpServer5000.listen(5000);
import http from "http";
const httpServer5001 = http.createServer();
httpServer5001.listen(5001);
httpServer5001.on("connection", (socket) => console.log("connection"));
瀏覽器訪問 http://localhost:5000/preconnect 的當下,就可以在 Node.js 的 log 看到 connection
用 Wireshark 抓包,也可以看到 TCP 3-way handshake

使用 Node.js http 模組測試
import http from "http";
import { readFileSync } from "fs";
import { join } from "path";
const httpServer5000 = http.createServer((req, res) => {
const url = new URL(req.url || "", "http://localhost:5000");
if (url.pathname === "/preload") {
res.setHeader(
"link",
`<http://localhost:5001/preload.js>; rel="preload"; as="script"`,
);
res.setHeader("Content-Type", "text/html");
res.end(readFileSync(join(import.meta.dirname, "preload.html")));
return;
}
});
httpServer5000.listen(5000);
<head>
<!-- 為了驗證 HTTP Link 的執行時機早於 HTML link,在 preload.html 也載入同樣資源,用 `?from=head` 來區分 -->
<xcript src="http://localhost:5001/preload.js?from=head"></xcript>
</head>
import http from "http";
const httpServer5001 = http.createServer((req, res) => {
const url = new URL(req.url || "", "http://localhost:5001");
if (url.pathname === "/preload.js") {
res.setHeader("Content-Type", "text/javascript");
res.end(`const preload = true;`);
return;
}
});
httpServer5001.listen(5001);
瀏覽器輸入 http://localhost:5000/preload ,可以看到 HTTP Link 確實更快載入

Content-Type !== text/html既然 HTTP Link 是 HTML <link> 的 alternative,那如果設定在 Content-Type !== text/html 還會生效嗎?
我在 Google Font 的 CSS 有看過這種設定

但還是寫個 PoC 來驗證
import { readFileSync } from "fs";
import http from "http";
import { join } from "path";
const httpServer5000 = http.createServer((req, res) => {
const url = new URL(req.url || "", "http://localhost:5000");
if (url.pathname === "/js-with-link-preload.js") {
res.setHeader(
"link",
`<http://localhost:5001/preload.js>; rel="preload"; as="script"`,
);
res.setHeader("Content-Type", "text/javascript");
res.end(`const hello = "world";`);
return;
}
});
httpServer5000.listen(5000);
瀏覽器訪問 http://localhost/js-with-link-preload.js ,有成功 preload,不過瀏覽器會跳 warning

在這篇文章,我們學到了
<link> 的優勢rel="preload" 也可以在 "非 HTML" 的 response 載入